/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
**/

package com.chasekernan.hxnova.core.races;

import com.chasekernan.hxnova.core.stars.Environment;
import haxe.xml.Check;

/**
    Contains the set of habital ranges for a race. Relies on Environment for the
    environment types. 
    Ranges are stored as objects having a lower and upper value (HabPart).
**/
class HabSetting {
    
    /**
        Minimum value of the lower part of the range.
    **/
    public static var MIN_VALUE : Int = 0;
    
    /**
        Maximum value of the upper part of a range.
    **/
    public static var MAX_VALUE : Int = 100;
    
    /**
        The minimum width of a range.
    **/
    public static var MIN_WIDTH : Int = 20;
    
    /**
        Default value for the race editor.
    **/
    public static var DEFAULT_LOWER : Int = 15;
    
    /**
        Default value for the race editor.
    **/
    public static var DEFAULT_UPPER : Int = 85;
    
    /**
        Checks to see if a given range fits the limitations.
    **/
    public static function checkValue(lower : Int, upper : Int) : Bool {
        if(lower < MIN_VALUE) return false;
        if(upper > MAX_VALUE) return false;
        if((upper - lower) < MIN_WIDTH) return false;
        else return true;
    }
    
    /**
        Returns the middle of the available hab range.
    **/
    public static function getMiddle() : Float {
        return (MAX_VALUE + MIN_VALUE) / 2;
    }
    
    
    public var values : Array<HabPart>;
    
    /**
        All values are set to the default min/max to start with.
    **/
    public function new() {
        values = new Array<HabPart>();
        for(type in Environment.ENV_TYPES) 
            values.push(new HabPart());
    }
    
    /**
        Returns the range for a env type as a HabPart (class).
        Throws an error if there is no such env type.
    **/
    public function getHabPart(type : String) : HabPart {
        for(i in 0...Environment.ENV_TYPES.length) {
            if(type == Environment.ENV_TYPES[i]) return values[i];
        }
        
        //if here, no such type
        throw "No such environment type: " + type;
    }
    
    /**
        Sets the upper boundry of a hab range. Throws an error if the value 
        is out of range or there is no such env type.
    **/
    public function setHabUpper(type : String, value : Int) {
        getHabPart(type).upperBound = value;
    }
    
    /**
        Sets the lower boundry of a hab range. Throws an error if the value 
        is out of range or there is no such env type.
    **/
    public function setHabLower(type : String, value : Int) {
        getHabPart(type).lowerBound = value;
    }
    
    public static var XML_TITLE = "hab_setting";
    
    public static function getXmlRules() :Rule {
        var indiv : Array<Rule> = new Array<Rule>();
        for(i in Environment.ENV_TYPES) 
            indiv.push(Rule.RNode(i, [Att("lower_bound"), Att("upper_bound")]));
        
        return Rule.RNode(XML_TITLE, [], Rule.RList(indiv, false));
    }
    
    /**
        Writes a hab setting to xml.
        Sample XML:
            <hab_setting>
                <gravity lower_bound="20" upper_bound="60">
                <temperature lower_bound="50" upper_bound="95">
                <radiation lower_bound="35" upper_bound="77">
            </environment>
    **/
    public static function writeToXml(hs : HabSetting) : Xml {
        var xml : Xml = Xml.parse('<' + XML_TITLE + '></' + XML_TITLE + '>')
                        .firstChild();
        
        for(i in Environment.ENV_TYPES) {
            var childXml = Xml.createElement(i);
            
            childXml.set('lower_bound', Std.string(
                    hs.getHabPart(i).lowerBound));
            childXml.set('upper_bound', Std.string(
                    hs.getHabPart(i).upperBound));
                    
            xml.addChild(childXml);
        }
        //check it 
        Check.checkNode(xml, getXmlRules());
        return xml;
    }
    
    /**
        Reads a hab setting xml file. Will throw an error if there are any 
        mistakes.
    **/
    public static function readFromXml(xml : Xml) : HabSetting {
        try {
            var hs = new HabSetting();
            //check type
            Check.checkNode(xml, getXmlRules());
            
            for(i in xml.elements()) {
                hs.setHabLower(i.nodeName, Std.parseInt(
                        i.get('lower_bound')));
                hs.setHabUpper(i.nodeName, Std.parseInt(
                        i.get('upper_bound')));
            }
            
            return hs;
        } catch (e : Dynamic) {
            throw "Bad hab setting xml: " + e;
        }
    }
    
    public function iterator () {
        return values.iterator();
    }

    /**
        Returns the perfect environment for a race. Usually used to set the HW
        env.
    **/
    public function getPerfectEnv() : Environment {
        var e = new Environment();
        for(i in Environment.ENV_TYPES) {
            e.setValue(i, Math.floor((getHabPart(i).upperBound + 
                                      getHabPart(i).lowerBound) / 2));
        }
        
        return e;
    }
}

/** 
    Class describing one hab range. Contains a upper and lower boundry and 
    methods for manipulating them.
**/
class HabPart {
    
    private var _lowerBound : Int;
    public var lowerBound(getLowerBound, setLowerBound) : Int;
    
    private var _upperBound : Int;
    public var upperBound(getUpperBound, setUpperBound) : Int;
    
    public var middle(getMiddle, null) : Float;
    public var width(getWidth, null) : Int;
    public var radius(getRadius, null) : Float;
    
    /**
        Creates a new HabPart with the default values unless they are specified.
    **/
    public function new(?lowerBound : Int, ?upperBound : Int) {
        _lowerBound = if (lowerBound == null) HabSetting.DEFAULT_LOWER else 
                                              lowerBound;
        _upperBound = if (upperBound == null) HabSetting.DEFAULT_UPPER else 
                                              upperBound;
    }
    
    private function getLowerBound() : Int {
        return _lowerBound;
    }
    
    private function setLowerBound(value : Int) : Int {
        if(HabSetting.checkValue(value, _upperBound)) _lowerBound = value;
        else throw "Value is out of range: " + Std.string(value);
        
        return _lowerBound;
    }
    
    private function getUpperBound() : Int {
        return _upperBound;
    }
    
    private function setUpperBound(value : Int) : Int {
        if(HabSetting.checkValue(_lowerBound, value)) _upperBound = value;
        else throw "Value is out of range: " + Std.string(value);
        
        return _upperBound;
    }
    
    private function getMiddle() : Float {
        return (upperBound + lowerBound) / 2;
    }
    
    private function getWidth() : Int {
        return upperBound - lowerBound;
    }
    
    private function getRadius() : Float {
        return getWidth() / 2;
    }
    
    /**
        Shifts the entire range to the left (i.e. decrease values) by the amount
        specified.
        Note: no error is thrown when the values exceed the bounds, instead they
        are just truncated at the limits.
    **/
    public function shiftLeft(?amount = 1) {
        if((lowerBound - amount) < HabSetting.MIN_VALUE) {
            //no need to throw an error, just shift it as far left as it can go
            amount = lowerBound - HabSetting.MIN_VALUE;
            lowerBound =  HabSetting.MIN_VALUE;
        } else {
            lowerBound -= amount;
        }
            
        upperBound -= amount;
    }
    
    /**
        Shifts the entire range to the right (i.e. increase values) by the 
        amount specified (defaults to 1).
        Note: no error is thrown when the values exceed the bounds, instead they
        are just truncated at the limits.
    **/
    public function shiftRight(?amount = 1) {
        if((upperBound + amount) > HabSetting.MAX_VALUE) {
            amount = HabSetting.MAX_VALUE - upperBound;
            upperBound = HabSetting.MAX_VALUE;
        } else {
            upperBound += amount;
        }
        lowerBound += amount;
        
    }
    
    /**
        Shrinks the entire range (i.e. lower increased, upper decreased) by the
        amount specified.
        Note: no error is thrown when the values exceed the bounds, instead 
        they are just truncated at the limits.
    **/
    public function shrink(?amount = 1) {
        var width = Math.floor(
                Math.max(0, (upperBound - amount) - (lowerBound + amount)));
        
        if(width < HabSetting.MIN_WIDTH) {
            var widthBefore = getWidth();
            var change = Math.floor((widthBefore - HabSetting.MIN_WIDTH) / 2);
            upperBound -= change;
            lowerBound += change;
        } else {
            upperBound -= amount;
            lowerBound += amount;
        }
    }
    
    /**
        Enlarges the entire range (i.e. lower decreased, upper increased) by the
        amount specified.
        Note: no error is thrown when the values exceed the bounds, instead they
        are just truncated at the limits.
    **/
    public function enlarge(?amount = 1) {
        var dif = 0;
        
        if(((lowerBound - amount) >= HabSetting.MIN_VALUE) && ((upperBound + amount) <= HabSetting.MAX_VALUE)) {
            upperBound += amount;
            lowerBound -= amount;
        } else if((lowerBound - amount) >= HabSetting.MIN_VALUE) {
            dif = HabSetting.MAX_VALUE - upperBound;
            upperBound = HabSetting.MAX_VALUE;
            lowerBound -= amount - dif;
        } else if((upperBound + amount) <= HabSetting.MAX_VALUE) {
            dif = lowerBound;
            lowerBound = HabSetting.MIN_VALUE;
            upperBound += amount - dif;
        } else {
            lowerBound = HabSetting.MIN_VALUE;
            upperBound = HabSetting.MAX_VALUE;
        }
    }
}

#if unit

import haxe.unit.TestCase;

class HabSettingTest extends haxe.unit.TestCase {
    public var hab : HabSetting;
    
    public function testHabSetting() {
        hab = new HabSetting();
        
        for (part in hab) {
            assertEquals(HabSetting.DEFAULT_UPPER, part.upperBound);
            assertEquals(HabSetting.DEFAULT_LOWER, part.lowerBound);
        }
        
        try {
            hab.getHabPart("gibberish");
            assertTrue(false);
        } catch (e : Dynamic) {
            
        }
        
        hab.setHabLower(Environment.ENV_TYPES[0], 45);
        assertEquals(45, hab.getHabPart(Environment.ENV_TYPES[0]).lowerBound);
        
        hab.setHabUpper(Environment.ENV_TYPES[1], 85);
        assertEquals(85, hab.getHabPart(Environment.ENV_TYPES[0]).upperBound);
        
        assertTrue(HabSetting.checkValue(21, 92));
        assertFalse(HabSetting.checkValue( -2, 43));
        assertFalse(HabSetting.checkValue(35, 456));
        assertFalse(HabSetting.checkValue(42, 41 + HabSetting.MIN_WIDTH));
        
        var env = hab.getPerfectEnv();
        assertEquals(65, env.getValue(Environment.ENV_TYPES[0]));
    }
    
    public function testHabPart() {
        var habPart : HabPart = new HabPart(25, 85);
        
        try {
            habPart.upperBound = 400;
            assertTrue(false);
        } catch (e : Dynamic) {}
        
        try {
            habPart.lowerBound = -200;
            assertTrue(false);
        } catch (e : Dynamic) { }
        
        try {
            habPart.lowerBound = 75;
            assertTrue(false);
        } catch (e : Dynamic) { }
        
        habPart.lowerBound = 35;
        habPart.upperBound = 95;
        
        assertEquals(35, habPart.lowerBound);
        assertEquals(95, habPart.upperBound);
        
        assertEquals(65.0, habPart.middle);
        assertEquals(30.0, habPart.radius);
        assertEquals(60, habPart.width);
        
        habPart.shiftLeft();
        assertEquals(34, habPart.lowerBound);
        assertEquals(94, habPart.upperBound);
        
        habPart.shiftLeft(500);
        assertEquals(HabSetting.MIN_VALUE, habPart.lowerBound);
        assertEquals(HabSetting.MIN_VALUE + 60, habPart.upperBound);
        
        habPart.lowerBound = 35;
        habPart.upperBound = 95;
        
        habPart.shiftRight();
        assertEquals(36, habPart.lowerBound);
        assertEquals(96, habPart.upperBound);
        
        habPart.shiftRight(500);
        assertEquals(HabSetting.MAX_VALUE - 60, habPart.lowerBound);
        assertEquals(HabSetting.MAX_VALUE, habPart.upperBound);
        
        habPart.enlarge();
        assertEquals(HabSetting.MAX_VALUE - 61, habPart.lowerBound);
        assertEquals(HabSetting.MAX_VALUE, habPart.upperBound);
        
        habPart.shrink();
        assertEquals(HabSetting.MAX_VALUE - 60, habPart.lowerBound);
        assertEquals(HabSetting.MAX_VALUE - 1, habPart.upperBound);
        
        habPart.enlarge();
        assertEquals(HabSetting.MAX_VALUE - 61, habPart.lowerBound);
        assertEquals(HabSetting.MAX_VALUE, habPart.upperBound);
        
        habPart.enlarge(500);
        assertEquals(HabSetting.MIN_VALUE, habPart.lowerBound);
        assertEquals(HabSetting.MAX_VALUE, habPart.upperBound);
        
        habPart.shrink(500);
        assertEquals(HabSetting.MIN_WIDTH, habPart.width);
    }
    
    public function testHabSettingXml() {
        hab = new HabSetting();
        hab.setHabLower(Environment.ENV_TYPES[0], 34);
        hab.setHabUpper(Environment.ENV_TYPES[1], 86);
        
        var xml = HabSetting.writeToXml(hab);
        for (element in xml.elements()) {
            assertEquals(hab.getHabPart(element.nodeName).lowerBound, 
                    Std.parseInt(element.get("lower_bound")));
            assertEquals(hab.getHabPart(element.nodeName).lowerBound, 
                    Std.parseInt(element.get("lower_bound")));
        }
        
        var readValues = HabSetting.readFromXml(xml);
        for (envType in Environment.ENV_TYPES) {
            assertEquals(hab.getHabPart(envType).lowerBound, 
                         readValues.getHabPart(envType).lowerBound);
            assertEquals(hab.getHabPart(envType).upperBound, 
                         readValues.getHabPart(envType).upperBound);
        }
    }
    
}
#end